package biz_error

import (
	"fmt"
	"io"
	"net/http"
	"strings"
)

//go:generate stringer -type=Code -output=http_string.go
type Code int

const (
	_ Code = iota
	invalidParameter
	internalError

	unknownError
	checkTokenError
	genTokenError
	databaseError
	cacheError
	decodeError
	encodeError
	packetReject
	uploadError
	conflict
	notfound
	transcodeError
	rateLimit
)

var (
	InvalidParam    = New(http.StatusBadRequest, invalidParameter)
	CheckTokenError = New(http.StatusBadRequest, invalidParameter, checkTokenError)
	UnknownError    = New(http.StatusInternalServerError, internalError, unknownError)
	GenTokenError   = New(http.StatusInternalServerError, internalError, genTokenError)
	DatabaseError   = New(http.StatusInsufficientStorage, internalError, databaseError)
	CacheError      = New(http.StatusInternalServerError, internalError, cacheError)
	DecodeError     = New(http.StatusBadRequest, invalidParameter, decodeError)
	EncodeError     = New(http.StatusInternalServerError, internalError, encodeError)
	PacketReject    = New(http.StatusBadRequest, invalidParameter, packetReject)
	UploadError     = New(http.StatusInsufficientStorage, internalError, uploadError)
	Conflict        = New(http.StatusConflict, invalidParameter, conflict)
	NotFound        = New(http.StatusNotFound, invalidParameter, notfound)
	TranscodeError  = New(http.StatusInternalServerError, internalError, transcodeError)
	RateLimitError  = New(http.StatusForbidden, rateLimit)
)

type Error struct {
	StatusCode  int  // http status code
	firstClass  Code // biz error code
	secondClass Code
	err         error
}

func (e *Error) Error() string {
	return fmt.Sprintf("%s : %v", e.ErrorCode(), e.err)
}

func (e *Error) Cause() error { return e.err }

func (e *Error) Unwrap() error { return e.err }

func (e *Error) Is(err error) bool {
	if another, ok := err.(*Error); ok {
		return another.firstClass == e.firstClass &&
			(e.secondClass == 0 || another.secondClass == e.secondClass)
	}
	return false
}

func (e *Error) As(target interface{}) bool {
	_, ok := target.(*Error)
	return ok
}

func (e *Error) clone() *Error {
	return &Error{
		StatusCode:  e.StatusCode,
		firstClass:  e.firstClass,
		secondClass: e.secondClass,
	}
}

func (e *Error) ErrorCode() string {
	if e.secondClass == 0 {
		return strings.Title(e.firstClass.String())
	}
	return fmt.Sprintf("%s.%s", strings.Title(e.firstClass.String()), strings.Title(e.secondClass.String()))
}

func (e *Error) Format(s fmt.State, verb rune) {
	switch verb {
	case 'v':
		if s.Flag('+') {
			fmt.Fprintf(s, "%s : %+v", e.ErrorCode(), e.Cause())
			return
		}
		fallthrough
	case 's':
		io.WriteString(s, e.Error())
	}
}

func New(status int, first Code, others ...Code) *Error {
	e := &Error{
		StatusCode: status,
		firstClass: first,
	}
	if len(others) > 0 {
		e.secondClass = others[0]
	}
	return e
}

func (e *Error) Wrap(err error) error {
	if err == nil {
		return nil
	}
	another := e.clone()
	another.err = err
	return another
}
